Make values()/items() honour dol wrapper transforms - #10
Merged
Conversation
A mongo store serves its whole (key, value) stream in one `find`, so
MongoCollectionReader implements a bulk-read protocol (iter_values,
iter_items, contains_value, contains_item) and its views use it.
That fast path did not compose. A dol Store wrapper forwards any attribute
it does not define to the store it wraps, so a view calling
`self._mapping.iter_values()` punched straight through every wrapper and
yielded raw documents -- silently skipping the value transforms the user
asked for. `wrap_kvs(s, obj_of_data=f)` gave a correct `ss[k]` and a wrong
`list(ss.values())`: a Mapping-contract violation producing wrong-typed data
for anyone iterating values rather than keys.
New module `mongodol/views.py` resolves the bulk stream explicitly instead of
relying on attribute delegation. It walks the wrapper chain inward, crossing
each layer whose read path is plain transform composition (Store's own
__getitem__/__iter__), until it reaches a store that really implements the
bulk-read protocol; the stream is then re-transformed by the crossed layers,
innermost first, so it lands in the same space as `store[k]`. A layer that
redefines __getitem__ or __iter__ (postget, filt_iter, cached_keys) cannot be
pushed onto a bulk stream, so the resolver raises NoBulkReadPath and the views
fall back to the generic per-key path: correct, just one round trip per key.
Containment (`v in s.values()`) follows the same rule in the ingoing
direction, and falls back when a value transform declares no inverse -- that
case previously reached pymongo with a non-document filter and raised.
Everything stays local to mongodol; dol is untouched. The change is opt-out
via the documented `disable_bulk_read` class decorator, for classes that
inherit bulk-read methods no longer matching their own __getitem__.
Also in this change, all surfaced by the new invariant tests:
- MongoBaseStore keeps working (mongodol.trans.wrap_kvs) and now forwards
through the resolver, so it composes with plain wrap_kvs layers too.
- MongoCollectionMultipleDocs{Reader,Persister} were entirely broken:
__getitem__ always raised (an obj_of_data-shaped function was passed as a
postget) and __setitem__ always raised (a stale `_mgc` attribute, plus a
Mapping treated as a collection of docs). Fixed, and declared
bulk-unfaithful since their values are *lists* of docs.
- Dropped tests/not_working.py: an uncollected TDD placeholder for exactly
this issue, now covered by tests/views_test.py.
Known gap, deliberately left and pinned with a strict xfail: with no
getitem_projection, iter_items pops the key fields out of the value, so
items() values lack '_id' while store[k] has it. Fixing that changes
behaviour tests/int_tests/base_int_test.py explicitly encodes.
Claude-Session: https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7.
The bug, reproduced
Against a live local MongoDB:
list(ss.values()) != [ss[k] for k in ss]is aMapping-contract violation, anda silent one: any code that iterates values rather than keys gets wrong-typed
data with no error. Containment was worse —
('Matthew', 42) in ss.values()handed a tuple to
find()as a filter and raisedOperationFailure.Why it happened
MongoCollectionReaderimplements a bulk-read protocol —iter_values,iter_items,contains_value,contains_item— so a wholevalues()view isone
findinstead of N. Its views calledself._mapping.iter_values().A
dolStorewrapper forwards every attribute it does not define to the storeit wraps. So that one call punched straight through the wrapper chain to the raw
mongo store, skipping every transform on the way. The efficiency win and the
transform were mutually exclusive, and the wrapper silently won.
MongoBaseStore(+mongodol.trans.wrap_kvs) was the existing workaround, butit only works if the user knows to reach for mongodol's
wrap_kvsinstead ofdol's — the leak the issue is about.The fix — resolve the bulk path, don't delegate to it
New module
mongodol/views.py. Instead of trusting attribute delegation, theviews walk the wrapper chain inward, remembering each layer they cross, until
they reach a store that really implements the bulk-read protocol. The bulk stream
is then re-transformed by the crossed layers, innermost first, so it lands in the
same space as
store[k].A layer may be crossed only when its read path is plain transform composition —
Store's own__getitem__/__iter__— because then its contribution is exactly_key_of_id/_obj_of_data, which map cleanly over a stream. A layer thatredefines
__getitem__(wrap_kvs(postget=...)) or__iter__(filt_iter,cached_keys) cannot be expressed that way, so the resolver refuses to guess:NoBulkReadPath, and the view falls back to the generic per-key path. Correct,just one round trip per key. Correctness first, efficiency only when provable.
Containment follows the same rule in the ingoing direction and additionally
requires an inverse: a layer with an
obj_of_databut nodata_of_objcannotpush a user-space value down to mongo, so it falls back to a scan rather than
sending nonsense to
find().Design notes:
dolis untouched — the fix lives where the bulkprotocol lives.
disable_bulk_readis the documented opt-out for a class thatinherits bulk-read methods no longer matching its own
__getitem__.MongoBaseStoreandmongodol.trans.wrap_kvskeep working unchanged from theoutside; their bulk methods now route through the resolver, so they compose
with plain
wrap_kvslayers too.views.pyis mongo-specific. It is a general answer to "how does astore with a bulk-read fast path compose with
dolwrappers?" and is areasonable candidate for
dolto own one day.Drive-by fixes (surfaced by the new invariant tests)
MongoCollectionMultipleDocsReader/...Persisterwere entirely non-functional:__getitem__always raised — anobj_of_data-shaped function was passed as apostget. Added the key-awarePostGet.all_docs_fetch.__setitem__always raised — a stale_mgcattribute, and aMappingtreatedas a collection of docs (a
Mappingis aCollection, so a single doc got"iterated" into its field names).
They are also declared bulk-unfaithful, since their values are lists of docs
while the inherited bulk stream yields single docs.
Removed
mongodol/tests/not_working.py: an uncollected TDD placeholder forexactly this issue, superseded by
mongodol/tests/views_test.py.Known gap, deliberately left
With no
getitem_projection,iter_itemspops the key fields out of the value,so
items()values lack_idwhilestore[k]has it. Fixing that changesresults
tests/int_tests/base_int_test.py::test_store_with_mappersexplicitlyasserts, so it needs a call rather than a unilateral rewrite. Filed as #9 and
pinned here with a
strict=Truexfail, which will flip the suite red the momentit is fixed.
Tests
mongodol/tests/views_test.py, 12 tests + 1 strict xfail, pinningacross: plain
wrap_kvsvalue and key transforms, stacked wrappers, theMongoBaseStoreroute, the two fallback routes (filt_iter, userpostget),the multiple-docs store, both containment directions, and the resolver's own
contract (including that
hasattrlies on aStorebutprovides_bulk_readdoes not — the delegation trap that made the bug silent).
Red first, verified: with the source reverted to
masterand only the newtest file in place, 8 of the 13 fail, including the issue reproduction. With the
fix, all pass.
pytest --doctest-modulespytestTen consecutive runs of each, green both ways.
Dependents (all against a live local MongoDB):
py2store8 passed,funds1 passed,invest/peruse/qono tests collected,know1 failed / 3 passed — that failure is a
zipfile.BadZipFileon a test fixture,identical on
master, unrelated.https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c